<?php
/**
 * Created by PhpStorm.
 * User: whwyy
 * Date: 2018/3/30 0030
 * Time: 14:39
 */
declare(strict_types=1);

namespace Database;


use Database\Base\ActiveQueryInterface;
use Exception;

defined('SAVE_FAIL') or define('SAVE_FAIL', 3227);
defined('FIND_OR_CREATE_MESSAGE') or define('FIND_OR_CREATE_MESSAGE', 'Create a new model, but the data cannot be empty.');

/**
 * Class Orm
 * @package Database
 *
 * @property $attributes
 * @property-read $oldAttributes
 *
 * @mixin ActiveQueryInterface
 */
class Model extends Base\Model
{


    /**
     * @param string $column
     * @param int|float $value
     * @return ModelInterface|false
     * @throws
     */
    public function increment(string $column, int|float $value): bool|ModelInterface
    {
        if (!$this->mathematics([$column => $value], '+')) {
            return false;
        }
        $this->{$column} += $value;
        return $this->refresh();
    }


    /**
     * @param string $column
     * @param int|float $value
     * @return ModelInterface|false
     * @throws
     */
    public function decrement(string $column, int|float $value): bool|ModelInterface
    {
        if (!$this->mathematics([$column => $value], '-')) {
            return false;
        }
        $this->{$column} -= $value;
        return $this->refresh();
    }


    /**
     * @param array $columns
     * @return ModelInterface|false
     * @throws
     */
    public function increments(array $columns): bool|static
    {
        if (!$this->mathematics($columns, '+')) {
            return false;
        }
        foreach ($columns as $key => $attribute) {
            $this->$key += $attribute;
        }
        return $this;
    }


    /**
     * @param array $columns
     * @return ModelInterface|false
     * @throws
     */
    public function decrements(array $columns): bool|static
    {
        if (!$this->mathematics($columns, '-')) {
            return false;
        }
        foreach ($columns as $key => $attribute) {
            $this->$key -= $attribute;
        }
        return $this;
    }

    /**
     * @param array $condition
     * @param array $attributes
     * @return bool|static
     * @throws
     */
    public static function findOrCreate(array $condition, array $attributes): bool|static
    {
        $model      = static::instance();
        $connection = $model->getConnection()->beginTransaction();
        try {
            /** @var static $select */
            $select = $model::query()->where($condition)->first();
            if ($select === null) {
                $select = $model::populate(array_merge($condition, $attributes));
                if (!$select->save()) {
                    throw new Exception('保存失败: ' . $model->getLastError());
                }
            }
            $connection->commit();
            return $select;
        } catch (\Throwable $throwable) {
            $connection->rollback();
            return \Kiri::getLogger()->failure($throwable);
        }
    }


    /**
     * @param array $condition
     * @param array $attributes
     * @return bool|static
     * @throws
     */
    public static function createOrUpdate(array $condition, array $attributes = []): bool|static
    {
        $model      = static::instance();
        $connection = $model->getConnection()->beginTransaction();
        try {
            /** @var static $select */
            $select = static::query()->where($condition)->first();
            if (empty($select)) {
                $select = static::populate($condition);
            }
            $select->attributes = $attributes;
            if (!$select->save()) {
                throw new Exception('保存失败: ' . $model->getLastError());
            }
            $connection->commit();
            return $select;
        } catch (\Throwable $throwable) {
            $connection->rollback();
            return \Kiri::getLogger()->failure($throwable);
        }
    }


    /**
     * @param string $name
     * @param array $arguments
     * @return Model|ActiveQuery
     */
    public static function __callStatic(string $name, array $arguments): static|ActiveQuery
    {
        // TODO: Change the autogenerated stub
        if (method_exists(static::class, $name)) {
            return call_user_func(static::class . '::' . $name, ...$arguments);
        } else {
            return static::query()->{$name}(...$arguments);
        }
    }


    /**
     * @param array $columns
     * @param string $action
     * @return array|bool|int|string|null
     */
    private function mathematics(array $columns, string $action): int|bool|array|string|null
    {
        $condition   = [$this->getPrimary() => $this->getPrimaryValue()];
        $activeQuery = static::query()->where($condition);
        $create      = SqlBuilder::builder($activeQuery)->mathematics($columns, $action);
        if (is_bool($create)) {
            return false;
        }
        return $activeQuery->buildCommand($create)->exec();
    }


    /**
     * @param array $params
     * @return ModelInterface|bool
     * @throws
     */
    public function update(array $params): static|bool
    {
        if (!$this->validator($this->rules(), $params) || !$this->beforeSave($this)) {
            return FALSE;
        }
        return $this->updateInternal(...$this->arrayIntersect($params));
    }


    /**
     * @param array $data
     * @return bool
     * @throws
     */
    public static function inserts(array $data): bool
    {
        if (empty($data)) {
            return trigger_print_error('Insert data empty.', 'mysql');
        }
        return static::query()->insert($data);
    }

    /**
     * @return bool
     * @throws
     */
    public function delete(): bool
    {
        if (!$this->beforeDelete()) {
            return false;
        }
        if ($this->hasPrimary()) {
            $result = static::deleteByCondition(['id' => $this->getPrimaryValue()]);
        } else {
            $result = static::deleteByCondition($this->_attributes);
        }
        return $this->afterDelete($result);
    }


    /**
     * @return array
     * @throws
     */
    public function toArray(): array
    {
        $data = $this->_attributes;
        foreach ($data as $key => $datum) {
            $method = 'get' . ucfirst($key) . 'Attribute';
            if (!method_exists($this, $method)) {
                continue;
            }
            $data[$key] = $this->{$method}($datum);
        }
        return $this->withs($data);
    }


    /**
     * @param array $data
     * @return array
     */
    private function withs(array $data): array
    {
        $with = $this->getWith();
        foreach ($with as $value) {
            $join = $this->withRelate($value);
            if ($join instanceof \Arrayable) {
                $join = $join->toArray();
            }
            $data[$value] = $join;
        }
        return $data;
    }


    /**
     * @param string $modelName
     * @param string $foreignKey
     * @param string $localKey
     * @return string
     * @throws Exception
     */
    private function _hasBase(string $modelName, string $foreignKey, string $localKey): string
    {
        if (($value = $this->{$localKey}) === null) {
            throw new Exception("Need join table primary key.");
        }

        $relation = di(Relation::class);

        $primaryKey = str_replace('\\', '_', $modelName) . '_' . $foreignKey . '_' . $value;
        if (!$relation->hasIdentification($primaryKey)) {
            $relation->bindIdentification($primaryKey, $modelName::query()->where([$foreignKey => $value]));
        }
        return $primaryKey;
    }


    /**
     * @param string $modelName
     * @param string $foreignKey
     * @param string $localKey
     * @return HasOne|ActiveQuery
     * @throws Exception
     */
    public function hasOne(string $modelName, string $foreignKey, string $localKey): HasOne|ActiveQuery
    {
        return new HasOne($this->_hasBase($modelName, $foreignKey, $localKey));
    }


    /**
     * @param string $modelName
     * @param string $foreignKey
     * @param string $localKey
     * @return ActiveQuery|HasCount
     * @throws Exception
     */
    public function hasCount(string $modelName, string $foreignKey, string $localKey): ActiveQuery|HasCount
    {
        return new HasCount($this->_hasBase($modelName, $foreignKey, $localKey));
    }


    /**
     * @param string $modelName
     * @param string $foreignKey
     * @param string $localKey
     * @return ActiveQuery|HasMany
     * @throws Exception
     */
    public function hasMany(string $modelName, string $foreignKey, string $localKey): ActiveQuery|HasMany
    {
        return new HasMany($this->_hasBase($modelName, $foreignKey, $localKey));
    }

    /**
     * @param string $modelName
     * @param string $foreignKey
     * @param string $localKey
     * @return ActiveQuery|HasMany
     * @throws Exception
     */
    public function hasIn(string $modelName, string $foreignKey, string $localKey): ActiveQuery|HasMany
    {
        if (($value = $this->{$localKey}) === null) {
            throw new Exception("Need join table primary key.");
        }

        $relation = di(Relation::class);

        $primaryKey = str_replace('\\', '_', $modelName) . '_' . $foreignKey . '_' . implode('_', $value);
        if (!$relation->hasIdentification($primaryKey)) {
            $relation->bindIdentification($primaryKey, $modelName::query()->whereIn($foreignKey, $value));
        }

        return new HasMany($primaryKey);
    }

    /**
     * @param bool $result
     * @return bool
     */
    public function afterDelete(bool $result): bool
    {
        return $result;
    }

    /**
     * @return bool
     * @throws Exception
     */
    public function beforeDelete(): bool
    {
        return TRUE;
    }
}
